package com.mathcat.mobile.android.widget;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Map;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import com.mathcat.mobile.android.R;

/**
 * A custom widget which can display mixed text and image content.
 * The image was referenced by text which is wrap by a pair of delimiters. 
 * 
 * @author Mathcat
 * @version 0.1, 2011.03.20
 */
public class TextImageView extends View {

	private final static int EOF = -1;
	
	private static Map<String, Bitmap> text2imageMap;
	
	private char startDelimiter;
	
	private char endDelimiter;
	
	private String rawText;
	
	private Paint paint;
	
	private LinkedList<Element> parsedElements = new LinkedList<Element>();
	
	private ArrayList<Element> measuredElements = new ArrayList<Element>();
	
	public TextImageView(Context context, char startDelimiter, char endDelimiter) {
		super(context);

		this.startDelimiter = startDelimiter;
		this.endDelimiter = endDelimiter;
		
		this.paint = new Paint();
		this.paint.setColor(Color.WHITE);
	}

	public TextImageView(Context context, AttributeSet attrs) {
		super(context, attrs);
		this.paint = new Paint();
		
		TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TextImageView);
		
		String startDelimiterStr = a.getString(R.styleable.TextImageView_startDelimiter);
		String endDelimiterStr = a.getString(R.styleable.TextImageView_endDelimiter);
		if (startDelimiterStr != null && endDelimiterStr != null 
				&& startDelimiterStr.length() > 0 && endDelimiterStr.length() > 0) {
			this.startDelimiter = startDelimiterStr.charAt(0);
			this.endDelimiter = endDelimiterStr.charAt(0);
		}
		
		String rawText = a.getString(R.styleable.TextImageView_rawText);
		if (rawText != null) {
			setRawText(rawText);
		}
		
		int textColor = a.getColor(R.styleable.TextImageView_textColor, Color.WHITE);
		setTextColor(textColor);
		
		int textSize = a.getDimensionPixelOffset(R.styleable.TextImageView_textSize, 16);
		setTextSize(textSize);
	}
	
	public static void setupText2ImageMap(Map<String, Bitmap> text2imageMap) {
		TextImageView.text2imageMap = text2imageMap;
	}
	
	public void setRawText(String rawText) {
		this.rawText = rawText;
		
		parseRawText(this.rawText);
		
		requestLayout();
		invalidate();
	}
	
	public void setTextColor(int textColor) {
		this.paint.setColor(textColor);
		
		invalidate();
	}
	
	public void setTextSize(int textSize) {
		this.paint.setTextSize(textSize);
		
		requestLayout();
		invalidate();
	}
	
	private void parseRawText(String rawText) {

		this.parsedElements.clear();
		
		Reader reader = new StringReader(rawText);
		StringBuffer buf = new StringBuffer();
		int ch;
		
		try { // StringReader will not actually throws IOException.
			
			while (true) {
				ch = reader.read();
				
				if (ch == EOF) {
					if (buf.length() != 0) {
						String text = buf.toString();
						
						Element element = new Element();
						element.text = text;
						element.type = Element.TYPE_TEXT;
						
						this.parsedElements.add(element);
					}
					
					break;
				}
				
				
				if (ch != this.startDelimiter) {
					buf.append((char) ch);
					
				} else {
					// Start delimiter character is read.
					if (buf.length() != 0) {
						String text = buf.toString();
						
						Element element = new Element();
						element.text = text;
						element.type = Element.TYPE_TEXT;
						this.parsedElements.add(element);
						
						buf.delete(0, buf.length());
					}
					
					// Begin processing the image referencing text.
					while (true) {
						ch = reader.read();
						
						if (ch == EOF) {
							// Read the EOF character but the end-delimiter character not read yet.
							// Treat the read but unclosed characters as a type TEXT element.
							String text = this.startDelimiter + buf.toString();
							
							Element element = new Element();
							element.text = text;
							element.type = Element.TYPE_TEXT;
							this.parsedElements.add(element);

							buf.delete(0, buf.length());
										
							break;
							
						} else if (ch == this.endDelimiter) {
							// Read the end-delimiter character.
							String text = buf.toString();
							
							Element element = new Element();
							element.text = text;
							element.type = Element.TYPE_IMAGE;
							this.parsedElements.add(element);

							buf.delete(0, buf.length());
							
							break;
							
						} else {
							buf.append((char) ch);
						}
					}
					
				}
			}
			
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
		
		for (Element element : parsedElements) {
			Log.d("PARSED", element.toString());
		}
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		
		this.measuredElements.clear();
		
		int widthMode = MeasureSpec.getMode(widthMeasureSpec), heightMode = MeasureSpec.getMode(heightMeasureSpec);
		int widthSize = MeasureSpec.getSize(widthMeasureSpec), heightSize = MeasureSpec.getSize(heightMeasureSpec);
		int widthBound = (widthMode == MeasureSpec.UNSPECIFIED) ? Integer.MAX_VALUE : widthSize;
		
		int xpos = 0, ypos = 0; // Current origin.
		int lineHeight = 0; // Current line height.
		int availWidth = widthBound; // Current available width at current line.
		int maxWidth = 0; // Keep track of the maximum line width.
		
		LinkedList<Element> parsedElements = (LinkedList<Element>) this.parsedElements.clone();
		
		while (!parsedElements.isEmpty()) {
			
			// Consume one element from the queue. 
			Element element = parsedElements.poll();
			
			// Calculate the element measurement.
			int w, h;
			if (element.type == Element.TYPE_IMAGE) {
				Bitmap bitmap = null;
				if ((bitmap = TextImageView.text2imageMap.get(element.text)) != null) {
					element.bitmap = bitmap;	
					w = bitmap.getWidth();
					h = bitmap.getHeight();
				
				} else {
					// Failed to retrieve the image referenced by the text.
					// Convert this element from type image to text.
					element.type = Element.TYPE_TEXT;
					
					String text = this.startDelimiter + element.text + this.endDelimiter;
					Rect rect = new Rect();
					this.paint.getTextBounds(text, 0, text.length(), rect);
					w = rect.right - rect.left;
					h = rect.bottom - rect.top;
				}
				
			} else {
				String text = element.text;
				Rect rect = new Rect();
				this.paint.getTextBounds(text, 0, text.length(), rect);
				w = rect.right - rect.left;
				h = rect.bottom - rect.top;
			}
			
			if (availWidth > w)	 {
				// Sufficient space.
				
				// Layout this element.
				element.x = xpos;
				element.y = ypos;
				
				xpos += w; // Adjust current x-pos. 
				availWidth -= w; // Diminish the available width.
				if (lineHeight < h)
					lineHeight = h; // Adjust current lineHeight.
				
				this.measuredElements.add(element);
				
			} else {
				// No sufficient space.
				// Should do line break.
				
				if (element.type == Element.TYPE_IMAGE) {
					// Extreme situation: if the width is too small to even fit the image in, drop it.
					if (widthBound < element.bitmap.getWidth())
						break;
					
					// Push back the element.
					parsedElements.addFirst(element);
					// Do line break.
					maxWidth = (widthBound - availWidth) > maxWidth ? (widthBound - availWidth) : maxWidth;
					xpos = 0; ypos += lineHeight; lineHeight = 0; availWidth = widthBound;
					
				} else {
					// Split the text type element.
					String text2Split = element.text;
					int cnt = paint.breakText(text2Split, true, availWidth, null);

					// Create a new Element of the second half of text.
					// And layout it.
					String firstPiece = text2Split.substring(0, cnt - 1);
					Element firstElement = new Element();
					firstElement.type = Element.TYPE_TEXT;
					firstElement.text = firstPiece;
					// Layout this element.
					element.x = xpos;
					element.y = ypos;
					
					this.measuredElements.add(element);
					
					if (lineHeight < h)
						lineHeight = h; // Adjust current lineHeight.
					
					// Create a new Element of the second half of text.
					// And push back to the parsed element queue.
					String secondPiece = text2Split.substring(cnt);
					Element secondElement = new Element();
					secondElement.type = Element.TYPE_TEXT;
					secondElement.text = secondPiece;
					parsedElements.addFirst(secondElement);
					
					// Do line break.
					maxWidth = (widthBound - availWidth) > maxWidth ? (widthBound - availWidth) : maxWidth;
					xpos = 0; ypos += lineHeight; lineHeight = 0; availWidth = widthBound;
				}
				
			}
		}
		
		ypos += lineHeight;
		
		int width, height;
		
		// Determine the dimension of itself.
		if (widthMode == MeasureSpec.EXACTLY) {
			width = widthSize;
		} else {
			width = maxWidth;
		}
		
		if (heightMode == MeasureSpec.EXACTLY) {
			height = heightSize;
		} else {
			height = ypos;
		}
		
		setMeasuredDimension(width, height);
	}
	
	@Override
	protected void onDraw(Canvas canvas) {
		int xprev = 0, yprev = 0;

		for (Element element : this.measuredElements) {
			int xcurr = element.x, ycurr = element.y;
			canvas.translate(xcurr - xprev, ycurr - yprev);
			
			if (element.type == Element.TYPE_IMAGE) {
				canvas.drawBitmap(element.bitmap, 0, 0, paint);
			} else {
				
				// TODO: how to get the height directly? If no, add w, h to Element?
				Rect rect = new Rect();
				paint.getTextBounds(element.text, 0, element.text.length(), rect);
				int h = rect.bottom - rect.top;
				
				canvas.drawText(element.text, 0, h, paint);
			}
			
			xprev = xcurr; yprev = ycurr;
		}
	}
	
	/**
	 * Elements that the raw text may contain.
	 * Text or image described in the raw text included by delimiter pair.
	 */
	private static class Element {
		
		final static int TYPE_TEXT = 1;
		final static int TYPE_IMAGE = 2;
		
		/** type of this Element */
		int type = TYPE_TEXT;
		
		/** origin position of this element */
		int x, y;
		
		/**
		 * If type is TYPE_TEXT, text refers to the raw text.
		 * If type is TYPE_IMAGE, text refers to the raw text referencing the image.
		 */
		String text;
		
		/**
		 * If type is TYPE_IMAGE, bitmap refers to the image referenced by the corresponding raw text.
		 * Or null.
		 */
		Bitmap bitmap;
		
		@Override
		public String toString() {
			return (type == TYPE_TEXT ? "TYPE_TEXT" : "TYPE_IMAGE") + ", " + text;
		}
	}
}
